﻿using System;
using System.Collections.Generic;
using System.Linq.Expressions;
using System.Reflection;
using Faml.Frontend;

namespace Faml.LinqCompiler
{
    public class LinqExpressionCompiler
    {
        private FamlExpression fexp;
        private Dictionary<FamlLambdaParameter, ParameterExpression> parameters;

        private static Dictionary<string, Type> types = new Dictionary<string, Type>();

        static LinqExpressionCompiler()
        {
            types.Add("int", typeof(int));
            types.Add("double", typeof(double));
            types.Add("double2", typeof(Vector2d));
            types.Add("string", typeof(string));
            types.Add("time", typeof(DateTime));
            types.Add("timeSpan", typeof(TimeSpan));
        }

        private LinqExpressionCompiler(FamlExpression famlExpression)
        {
            parameters = new Dictionary<FamlLambdaParameter, ParameterExpression>();
            this.fexp = famlExpression;
        }

        private Expression GetLinqExpression()
        {
            return GetLinqExpression(fexp);
        }

        //private int AddParameters(IList<ParameterExpression> args)
        //{
        //    foreach (ParameterExpression pe in args)
        //            parameters.Add(pe);
        //    return args.Count;
        //}


        private void AddParameter(FamlLambdaParameter famlP, ParameterExpression linqP)
        {
            if (!parameters.ContainsKey(famlP))
                parameters[famlP] = linqP;
        }

        //private void RemoveParameters(int count)
        //{
        //    if (parameters.Count < count)
        //        throw new ArgumentException("Number of parameters to remove is too large!");
        //    parameters.RemoveRange(parameters.Count - count, count);
        //}

        private ParameterExpression LookupParameter(FamlLambdaParameter s)
        {
            return parameters[s];
//            ParameterExpression pe = parameters.FindLast(p => p.Name == name);
//#if DEBUG
//            if (pe == null)
//                throw new ApplicationException("Parameter " + name + " is not found in stack");
//#endif 
//            return pe;
        }

        private Expression ImplicitConversion(Expression expression, Type destType)
        {
            return Expression.Convert(expression, destType);
        }

        private Expression CompileDictionary(FamlDictionaryExpression expr)
        {
            string[] keys = expr.GetKeys();
            List<Expression> keyExprs = new List<Expression>();
            foreach (string key in keys)
                keyExprs.Add(Expression.Constant(key, typeof(string)));
            List<Expression> valueExprs = new List<Expression>();
            foreach (string key in keys)
                valueExprs.Add(Expression.Convert(GetLinqExpression(expr.GetField(key)), typeof(object)));
            Expression keysArray = Expression.NewArrayInit(typeof(string),keyExprs);
            Expression valuesArray = Expression.NewArrayInit(typeof(object), valueExprs);
            MethodInfo mi = typeof(DictionaryHelper).GetMethod("CreateDictionary");
            mi = mi.MakeGenericMethod(typeof(string),typeof(object));
            return Expression.Call(mi, keysArray, valuesArray);
        }

        private Expression GetLinqExpression(FamlExpression faml)
        {
            switch (faml.NodeType)
            {
                case FamlExpressionType.Dictionary:
                    return CompileDictionary((FamlDictionaryExpression)faml);

                case FamlExpressionType.FieldAccess:
                    FamlFieldExpression mfe = (FamlFieldExpression)faml;

                    Expression exp = GetLinqExpression(mfe.Target);
                    MemberInfo member = exp.Type.GetMember(mfe.Field)[0];

                    return Expression.MakeMemberAccess(exp, member);


                case FamlExpressionType.UnaryExpression:
                    FamlUnaryExpression fue = (FamlUnaryExpression)faml;

                    Expression target = GetLinqExpression(fue.Target);
                    return Expression.MakeUnary(GetExpressionType(fue.Operator), target, fue.Type);


                case FamlExpressionType.BinaryExpression:
                    FamlBinaryExpression mbe = (FamlBinaryExpression)faml;

                    Expression left = GetLinqExpression(mbe.Left);
                    Expression right = GetLinqExpression(mbe.Right);

                    if (left.Type != right.Type)
                    {
                        Type destType;
                        if (CheckTypes(mbe.Operator, left.Type, right.Type, out destType))
                        {
                            if (left.Type != destType)
                                left = ImplicitConversion(left, destType);
                            if (right.Type != destType)
                                right = ImplicitConversion(right, destType);
                        }
                    }

                    return Expression.MakeBinary(GetExpressionType(mbe.Operator), left, right);



                case FamlExpressionType.Constant:
                    FamlConstant fconst = (FamlConstant)faml;
                    return Expression.Constant(fconst.Value, fconst.Type);



                case FamlExpressionType.NewArray:
                    FamlInlineArrayExpression mae = (FamlInlineArrayExpression)faml;

                    Expression[] items = new Expression[mae.Items.Count];
                    for (int i = 0; i < items.Length; i++)
                    {
                        items[i] = GetLinqExpression(mae.Items[i]);
                    }

                    Type itemType;
                    if (items.Length == 0)
                        itemType = typeof(int);
                    else
                        itemType = items[0].Type;

                    return Expression.NewArrayInit(itemType, items);


                case FamlExpressionType.ConditionalExpression:
                    FamlConditionalExpression fce = (FamlConditionalExpression)faml;
                    return Expression.Condition(GetLinqExpression(fce.Condition),
                        GetLinqExpression(fce.TrueBranch), GetLinqExpression(fce.FalseBranch));


                case FamlExpressionType.LambdaParameter:
                    return LookupParameter(((FamlLambdaParameterExpression)faml).Parameter);



                case FamlExpressionType.Lambda:
                    FamlLambdaExpression mle = (FamlLambdaExpression)faml;

                    ParameterExpression[] parameters = new ParameterExpression[mle.Parameters.Count];
                    for (int i = 0; i < parameters.Length; i++)
                    {
                        if (this.parameters.ContainsKey(mle.Parameters[i]))
                        {
                            parameters[i] = this.parameters[mle.Parameters[i]];
                        }
                        else
                        {
                            parameters[i] = GetLinqParameterExpression(mle.Parameters[i]);
                            this.parameters.Add(mle.Parameters[i], parameters[i]);
                        }
                    }

                    //int count = AddParameters(parameters);
                    Expression body = GetLinqExpression(((FamlUnaryExpression)mle.Body).Target);
                    Expression lambda = Expression.Lambda(body, parameters);
                    //RemoveParameters(count);

                    return lambda;


                case FamlExpressionType.FunctionCall:
                    FamlFunctionCallExpression mfunc = (FamlFunctionCallExpression)faml;

                    //Type[] types = new Type[mfunc.Arguments.Count];
                    Expression[] args = new Expression[mfunc.Arguments.Count];
                    for (int i = 0; i < args.Length; i++)
                    {
                        args[i] = GetLinqExpression(mfunc.Arguments[i]);

                        //   if (args[i] is LambdaExpression)
                        //       types[i] = args[i].GetType();
                        //   else
                        //       types[i] = args[i].Type;
                    }

                    ///  MethodInfo method = oplib.GetMethod(mfunc.Function, types);                

                    //  return Expression.Call(method, args);

                    return CompileOperation(mfunc.Operation, args);

                case FamlExpressionType.ExpressionReference:
                    FamlReferenceExpression famlRef = (FamlReferenceExpression)faml;
                    return Expression.Constant(famlRef.ReferencedContainer.Value, famlRef.Type);

            }            
            
            throw new LinqCompilerException("Unexpected faml expression node: \"" + faml.NodeType + "\"");
        }

        private MethodCallExpression CompileOperation(Operation op, Expression[] args)
        {
            if(op.IsStatic)
                return Expression.Call(op.Method, args);

            return Expression.Call(Expression.Constant(op.InstanceObject, op.InstanceObject.GetType()), op.Method, args);
        }

        private ExpressionType GetExpressionType(UnaryOperations unaryOperations)
        {
            switch (unaryOperations)
            {
                case UnaryOperations.Positive:
                    return ExpressionType.UnaryPlus;

                case UnaryOperations.Negate:
                    return ExpressionType.Negate;

                case UnaryOperations.Quote:
                    return ExpressionType.Quote;

                default:
                    throw new ApplicationException("Unexpected unary operation: " + unaryOperations);
            }
        }
        
        private static Type[] numericTypes = new Type[] { typeof(int), typeof(double) };

        private bool CheckTypes(BinaryOperations op, Type a, Type b, out Type destType)
        {
            destType = a;

            if (op == BinaryOperations.And || op == BinaryOperations.Or)
            {
                if(a == typeof(bool) && b == typeof(bool))
                    return false;
                throw new LinqCompilerException("Expecting for boolean types");
            }

            if (a == b) return false;
            int i;
            if ((i = Array.IndexOf<Type>(numericTypes, a)) >= 0)
            {
                int j = Array.IndexOf<Type>(numericTypes, b);
                if (j < 0) return false;

                i = Math.Max(i, j);
                destType = numericTypes[i];
                return true;
            }

            return false;
        }

        private ExpressionType GetExpressionType(BinaryOperations @operator)
        {
            switch (@operator)
            {
                case BinaryOperations.And:
                    return ExpressionType.And;
                case BinaryOperations.Or:
                    return ExpressionType.Or;
                case BinaryOperations.Equal:
                    return ExpressionType.Equal;
                case BinaryOperations.NotEqual:
                    return ExpressionType.NotEqual;
                case BinaryOperations.GreaterThan:
                    return ExpressionType.GreaterThan;
                case BinaryOperations.LessThan:
                    return ExpressionType.LessThan;
                case BinaryOperations.GreaterThanOrEqual:
                    return ExpressionType.GreaterThanOrEqual;
                case BinaryOperations.LessThanOrEqual:
                    return ExpressionType.LessThanOrEqual;
                case BinaryOperations.Add:
                    return ExpressionType.Add;
                case BinaryOperations.Subtract:
                    return ExpressionType.Subtract;
                case BinaryOperations.Multiply:
                    return ExpressionType.Multiply;
                case BinaryOperations.Divide:
                    return ExpressionType.Divide;
                case BinaryOperations.Modulo:
                    return ExpressionType.Modulo;
                default:
                    throw new LinqCompilerException("Unknown operator \"" + @operator + "\"");
            }
        }

        private ParameterExpression GetLinqParameterExpression(FamlLambdaParameter parameter)
        {
            ParameterExpression pexp = Expression.Parameter(parameter.Type,parameter.Name);
            return pexp;
        }

        /// <summary>
        /// Compiles Faml Expression to Linq Expression
        /// </summary>
        public static Expression Compile(FamlExpression famlExpression)
        {
            LinqExpressionCompiler compiler = new LinqExpressionCompiler(famlExpression);
            return compiler.GetLinqExpression();
        }
    }

    public class LinqCompilerException : ApplicationException
    {
        public LinqCompilerException(string message) : base(message)
        {

        }
    }
}
